package com.springboot.frame.starter.pay.v3;

import com.springboot.frame.starter.config.WxConfig;
import com.springboot.frame.starter.pay.v2.WXPayConstants;
import com.springboot.frame.starter.pay.v3.resp.*;
import com.springboot.frame.starter.pay.v3.resq.FacilV3PayProfitSharingResqBody;
import com.springboot.frame.starter.pay.v3.resq.FacilV3PayResqBody;
import com.springboot.frame.starter.tools.AesUtil;
import com.springboot.frame.starter.tools.PemUtil;
import com.springboot.frame.starter.tools.WxJsonUtils;
import com.springboot.frame.starter.tools.WxUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author liheng
 * @ClassName WxV3Pay
 * @Description
 * @date 2021-08-31 21:12
 */
public class WxV3Pay {
    /**
     * 请求成功相应码
     */
    private static final int STATUS_CODE = 200;
    private static final String HTTPS = "https://";

    private WxConfig config;
    private AutoUpdateCertificatesVerifier verifier;
    private WechatPayHttpClientBuilder builder;
    private CloseableHttpClient httpClient;
    private PrivateKeySigner privateKeySigner;
    private WechatPay2Validator validator;
    private PrivateKey privateKey;

    /**
     * 初始化
     *
     * @param config
     */
    public WxV3Pay(WxConfig config) {
        // 检查v3支付配置信息
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("开始检查v3支付配置信息....");
        }
        this.config = config;
        try {
            checkWxConfig();
        } catch (Exception e) {
            // 打印异常信息
            WxUtils.warn("检查v3支付配置信息出现错误：{}", e.getMessage());
            return;
        }
        this.privateKey = PemUtil.loadPrivateKey(this.config.getV3().getPrivateKeyStream());
        this.privateKeySigner = new PrivateKeySigner(this.config.getV3().getMchSerialNo(), this.privateKey);
        this.verifier = new AutoUpdateCertificatesVerifier(
                new WechatPay2Credentials(this.config.getMchId(), this.privateKeySigner),
                this.config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
        this.validator = new WechatPay2Validator(this.verifier);
        this.builder = WechatPayHttpClientBuilder.create()
                .withMerchant(this.config.getMchId(), this.config.getV3().getMchSerialNo(), this.privateKey)
                .withValidator(this.validator);
        this.httpClient = this.builder.build();
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("检查v3支付配置信息完成，未出现异常....");
        }
    }


    /**
     * 检查支付配置信息
     *
     * @throws Exception
     */
    private void checkWxConfig() throws Exception {
        if (this.config == null) {
            throw new Exception("config is null");
        }
        if (this.config.getAppId() == null || this.config.getAppId().trim().length() == 0) {
            throw new Exception("appid in config is empty");
        }
        if (this.config.getMchId() == null || this.config.getMchId().trim().length() == 0) {
            throw new Exception("MchID in config is empty");
        }
        if (this.config.getV3().getMchSerialNo() == null) {
            throw new Exception("mchSerialNo in config is empty");
        }
        if (this.config.getV3().getPrivateKeyStream() == null) {
            throw new Exception("cert stream in config is empty");
        }
        if (this.config.getWXPayDomain() == null) {
            throw new Exception("config.getWXPayDomain() is null");
        }

        if (this.config.getHttpConnectTimeoutMs() < 10) {
            throw new Exception("http connect timeout is too small");
        }
        if (this.config.getHttpReadTimeoutMs() < 10) {
            throw new Exception("http read timeout is too small");
        }

    }


    /**
     * jsApi下单--服务商
     *
     * @param requestBody
     */
    public Map<String, Object> jsApiPayFacil(FacilV3PayResqBody requestBody) {
        // 封装基础数据
        requestBody.setSpMchid(this.config.getMchId());
        requestBody.setSpAppid(this.config.getAppId());
        requestBody.setNotifyUrl(this.config.getV3().getFacilNotifyUrl());
        String reqBody = WxJsonUtils.toJsonString(requestBody);
        FacilV3PayRespBody body = preOrder(WXPayConstants.V3_FACIL_JSAPI_URL_SUFFIX, reqBody);
        String prepayId = body.getPrepayId();
        return wxTuneUp(prepayId);
    }

    /**
     * 分账--服务商
     */
    public FacilV3PayProfitSharingRespBody profitSharingFacil(FacilV3PayProfitSharingResqBody requestBody) {
        // 封装基础数据
        requestBody.setAppid(this.config.getAppId());
        requestBody.setSubMchid(this.config.getMchId());
        String reqBody = WxJsonUtils.toJsonString(requestBody);
        HttpEntityEnclosingRequestBase httpPost = requestPost(WXPayConstants.V3_PROFITSHARING_SUFFIX, reqBody);
        String respBody = result(httpPost);
        FacilV3PayProfitSharingRespBody facilBody = WxJsonUtils.parseObject(respBody, FacilV3PayProfitSharingRespBody.class);
        return facilBody;
    }

    /**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
     *
     * @param prepayId 微信下单返回的prepay_id
     * @param prepayId 应用ID(appid)
     * @return 当前调起支付所需的参数
     * @throws Exception
     */
    public Map<String, Object> wxTuneUp(String prepayId) {
        String appId = this.config.getAppId();
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = WxUtils.generateNonceStr();
        String packageStr = "prepay_id=" + prepayId;
        //加载签名
        String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
        String packageSign = this.privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).sign;
        Map<String, Object> map = new HashMap<>(6);
        map.put("appid", appId);
        map.put("timeStamp", timeStamp);
        map.put("nonceStr", nonceStr);
        map.put("packages", packageStr);
        map.put("signType", "RSA");
        map.put("paySign", packageSign);
        return map;
    }

    /**
     * 下单
     *
     * @param uri
     * @param reqdata
     * @throws Exception
     */
    public FacilV3PayRespBody preOrder(String uri, String reqdata) {
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(uri, reqdata);
        String payBody = result(httpPost);
        FacilV3PayRespBody respV3PayBody = WxJsonUtils.parseObject(payBody, FacilV3PayRespBody.class);
        return respV3PayBody;
    }


    /**
     * 构建方法请求
     *
     * @param uri
     * @param reqdata
     * @return
     */
    public HttpPost requestPost(String uri, String reqdata) {
        //请求URL
        HttpPost httpPost = new HttpPost(HTTPS + WXPayConstants.DOMAIN_API + uri);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(this.config.getHttpReadTimeoutMs())
                .setConnectTimeout(this.config.getHttpConnectTimeoutMs()).build();
        httpPost.setConfig(requestConfig);
        StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Content-type", "application/json");
        httpPost.setHeader("Accept", "application/json");
        return httpPost;
    }


    /**
     * v3 支付异步通知验证签名
     *
     * @param request
     * @return 异步通知明文
     * @throws Exception 异常信息
     */
    public FacilNotifyV3PayDecodeRespBody verifyFacilNotify(HttpServletRequest request) throws Exception {
        String notifyBody = WxUtils.streamBodyByReceive(request);
        // 回调验签
        boolean flag = this.validator.validate(request, notifyBody);
        if (flag) {
            FacilV3PayNotifyRespBody facilV3PayNotifyRespBody = WxJsonUtils.parseObject(notifyBody, FacilV3PayNotifyRespBody.class);
            FacilV3PayNotifyRespBody.Resource resource = facilV3PayNotifyRespBody.getResource();
            String cipherText = resource.getCiphertext();
            String nonceStr = resource.getNonce();
            String associatedData = resource.getAssociatedData();
            AesUtil aesUtil = new AesUtil(this.config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
            // 密文解密
            String decodeStr = aesUtil.decryptToString(
                    associatedData.getBytes(StandardCharsets.UTF_8),
                    nonceStr.getBytes(StandardCharsets.UTF_8),
                    cipherText);
            FacilNotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(decodeStr, FacilNotifyV3PayDecodeRespBody.class);
            return body;
        }
        throw new RuntimeException("验签失败");
    }

    /**
     * 请求结果
     *
     * @param request
     * @return
     */
    public String result(HttpRequestBase request) {
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(request);
            int statusCode = response.getStatusLine().getStatusCode();
            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            if (WxUtils.getLogger().isDebugEnabled()) {
                WxUtils.debug("请求成功：{}", respBodyStr);
            }
            if (STATUS_CODE == statusCode) {
                return respBodyStr;
            } else {
                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
                FacilV3PayErroRespBody respV3Body = WxJsonUtils.parseObject(respBodyStr, FacilV3PayErroRespBody.class);
                throw new RuntimeException("状态:" + statusCode + "," + respV3Body.getErrorCode() + ":" + respV3Body.getErrorMsg());
            }
        } catch (ConnectTimeoutException e) {
            throw new RuntimeException("接口超时");
        } catch (SocketTimeoutException e) {
            throw new RuntimeException("读取接口数据超时");
        } catch (IOException e) {
            throw new RuntimeException("接口请求失败，请尝试检查网络环境或请求接口是否能正常访问");
        } finally {
            // 关闭响应
            try {
                if (response != null) {
                    response.close();
                }
                //httpClient.close();
            } catch (IOException e) {
                throw new RuntimeException("关闭流异常");
            }
        }
    }

    /**
     * 微信结果确认应答
     * 支付通知http应答码为200或204才会当作正常接收，当回调处理异常时，应答的HTTP状态码应为500，或者4xx。
     * @param response
     * @throws IOException
     */
    public void ack(HttpServletResponse response) throws IOException {
        if (Objects.nonNull(response)) {
            PrintWriter writer = response.getWriter();
            writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
            if (Objects.nonNull(writer)) {
                writer.close();
            }
        }
    }
}
